[001] [RISC

您所在的位置:网站首页 gcc ld链接脚本 [001] [RISC

[001] [RISC

2024-07-17 03:15:43| 来源: 网络整理| 查看: 265

RISC-V Contents 基础概念 常用语法 使用示例 1 基础概念

image-20220819172031382

▲ ARM工具链软件编译流程

目标文件:程序源文件在经过编译器/汇编器 编译后会生成.o格式的文件,一般分为3种:

可重定位的目标文件(relocatable files):汇编器生成,是不可执行的。可执行的目标文件(executable files):经过链接器的链接、重定位后生成的可执行目标文件。可被共享的目标文件(shared object files):一般以共享库的形式存在,在程序运行时需要动态加载到内存,跟应用程序一起运行。

链接器:多个目标文件.o和库文件.a输入文件链接成一个可执行输出文件.elf,链接器从链接脚本读完一个 section 后,将重定位符号的值增加该 section 的大小。

section :一个可执行文件通常由不同的section(段)构成:text代码段、data数据段、bss段、rodata只读数据段等。每个section用一个section header来描述,包括段名、段的类型、段的起始地址、段的偏移和段的大小等。将这些section headers集中放到一起即为section header table(节头表)。

请添加图片描述

▲ Important Sections

详见:GNU ELF special sections

符号表:在「汇编阶段」,汇编器会分析汇编语言中各个section的信息,收集各种符号,生成符号表,将各个符号在section内的偏移地址、类型、占用空间的大小也填充到符号表内。(符号表本身也以section的形式添加到每一个可重定位目标文件中)

一个可执行文件中的所有符号都有自己的地址,并保存在「全局符号表」中,但此时「全局符号表」中的地址还都是原来在各个目标文件中的地址,即相对于零地址的偏移。

「Q」链接生成的可执行文件最终是要被加载到内存中执行的,那么要加载到内存中的什么地方呢?

「A」程序在链接程序时需要指定一个链接起始地址,链接开始地址一般也就是程序要加载到内存中的地址,通过链接脚本指定程序的链接地址和各个段的组装顺序。

链接脚本:主要用于规定各输入文件中的程序、数据等内容段在输出文件中的空间和地址如何分配。通俗的讲,链接脚本用于描述输入文件中的段,将其映射到输出文件中,并指定输出文件中的内存分配。

链接器就是根据链接脚本定义的规则来组装可执行文件的,并最终将这些信息以section的形式保存到可执行文件的ELF Header中。完整的ELF文件组织结构如下图所示:

image-20220824141043249

▲ Executable and Linkable Format 文件组织结构 2 常用语法 2.1 定位符 .

定位符 . 表示当前地址,它是一个变量,总是代表输出文件中的一个地址(根据输入文件section的大小不断增加,不能倒退,且只用于SECTIONS指令中)。对定位符 . 赋值可指定其后内容的存储位置,如果没有以其它的方式指定输出节的地址,则地址值就会被设为定位计数器的当前值,下面举例说明:

SECTIONS { . = 0x10000; .text : { *(.text) } . = 0x8000000; .data : { *(.data) } .bss : { *(.bss) } }

使用SECTIONS来描述输出文件各段的内存布局,在SECTIONS命令的开始处, 定位计数器当前值为0。

.= 0x10000:定位器当前值赋为0x10000.text即定义text代码段,且其定义时的地址即为定位器的当前值0x10000,通配符*代表所有的输入文件,即代表所有参与链接文件中的.text段(*main.o(.text)代表main.o文件中所有.text段)同理,.data即定义数据段,其地址为定时器当前值0x8000000, *(.data) 代表所有参与链接文件中的.data段;(*(.data.*)则表示所有参与链接文件的data段中的全部数据)紧跟data段后的即为bss段,其首地址为0x8000000 + .data section length。

下图为各文件 .text section .data section .bss section链接分配的示意图:

image-20220822153815351

注意:链接脚本从上往下,如果输入文件 A 已经被取出 .text section,此后输入文件 A 就没有 .text section,不能再被获取。

2.2 入口地址

ENTRY(SYMBOL):将符号 SYMBOL 的值设置为入口地址,入口地址是程序执行的第一条指令在程序地址空间的地址(如 ENTRY(Reset_Handler) 表示程序最开始从复位中断服务函数处执行)

有多种方法设置进程入口地址,以下编号越小,优先级越高:

1、ld 命令行的 -e 选项

2、链接脚本的 ENTRY(SYMBOL) 命令(如ENTRY( _start ))

3、在汇编程序中定义了 start 符号,使用 start 符号值(如.global _start)

4、如果存在 .text section,使用 .text section 首地址的值

5、使用地址 0 的值

声明了程序入口地址为_start后,在启动文件中会让其跳转到复位向量表中:

.global _start .align 1 _start: j handle_reset 2.3 MEMORY MEMORY { NAME1 [(ATTR)] : ORIGIN = ORIGIN1, LENGTH = LEN2 NAME2 [(ATTR)] : ORIGIN = ORIGIN2, LENGTH = LEN2 }

MEMORY命令定义了存储空间。

NAME:内存区域的名字,每一块内存区域必须有一个唯一的名字ATTR:定义该存储区域的属性。ATTR属性内可以出现以下7 个字符: R 只读sectionW 读/写sectionX 可执行sectionA 可分配的sectionI 初始化了的sectionL 同I! 反转以上任何属性的意义 ORIGIN:地址空间的起始地址,可缩写为org或o(但不能写成ORG)LENGTH:地址空间的长度,可缩写为len或l

可单独使用ORIGIN(memory)和LENGTH(memory)命令获取内存区域的起始地址以及长度。

使用示例:

MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K RAM (xrw) : o = 0x20000000, l = 20K } FLASH属性只读、可执行,起始地址为0x00000000,大小为64KRAM属性读写、可执行,起始地址为0x20000000,大小为20K 2.4 PROVIDE

该关键字定义一个(输入文件内被引用但没定义)符号。相当于定义一个全局变量的符号表,其他C文件可以通过该符号来操作对应的存储内存。

.bss : { . = ALIGN(4); PROVIDE( _sbss = .); *(.sbss*) *(.gnu.linkonce.sb.*) *(.bss*) *(.gnu.linkonce.b.*) *(COMMON*) . = ALIGN(4); PROVIDE( _ebss = .); } >RAM AT>FLASH

PROVIDE( _sbss = .)定义了bss段的起始地址_sbss,PROVIDE( _ebss = .)定义的bss段的结束地址_ebss。可在启动文件中调用该符号执行bss段清零操作:

/* clear bss section */ la a0, _sbss ; 将bss段起始地址_sbss加载到r0 la a1, _ebss ; 将bss段结束地址_ebss加载到r1 bgeu a0, a1, 2f ; 若a0 >= a1,则跳转到2处 1: sw zero, (a0) ; sw即store word,以字为单位将a0地址中存储的值清零 addi a0, a0, 4 ; a0 += 4 bltu a0, a1, 1b ; 若a0 < a1,则跳转到1处 2:

其中,数字标签1:用于本地引用。后缀为f表示向前跳转;后缀为b表示向后跳转。

注意:经过测试,实际上不加PROVIDE关键字,在链接文件中定义的变量(符号)也可以在目标文件中直接使用。

2.5 HIDDEN

语法:HIDDEN (symbol = expression),对于ELF目标端口,符号将被隐藏且不被导出(输出文件中不可见),示例:

HIDDEN (private_symbol = .); 2.6 PROVIDE_HIDDEN

语法:PROVIDE_HIDDEN (symbol = expression),是PROVIDE 和HIDDEN的结合体,类似于局部变量(外部程序不能使用)。示例:

PROVIDE_HIDDEN (__preinit_array_start = .); 2.7 SECTIONS结构 SECTIONS { ... secname [start_ADDR] [(TYPE)] : [AT (LMA_ADDR)] { contents } [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP] ... }

[ ]内的内容是可选项

secname:表示输出文件的 section 名

contents:描述输出文件的 section 内容是从哪些输入文件(目标文件.o和库文件.a)的哪些 section 里抽取而来

VMA(virtual memory address):虚地址,即输出文件运行地址

LMA(load memory address):加载地址,即数据实际存储的地址

数据段加载时会存至Flash中(使用LMA地址),一般需通过「重定位」将其搬运到RAM(使用VMA地址)。

start_addr :表示将某个段强制链接到的地址( VMA ),start_addr会改变定位符.的值。

TYPE:每个输出section都有一个类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section的类型。它可以为:

NOLOAD :该section在程序运行时,不被载入内存。DSECT,COPY,INFO,OVERLAY :这些类型很少被使用,为了向后兼容才被保留下来。这种类型的section必须被标记为「不可加载的」,以便在程序运行不为它们分配内存。

AT( LAM_ADDR ):输出 section 的 LMA(加载地址),默认情况下 LMA = VMA,但可以通过关键字 AT() 指定 LMA。

REGION:即前文所述用MEMORY命令定义的存储区域。

示例:

__stack_size = 2048; PROVIDE( _stack_size = __stack_size ); SECTIONS { ... .data : { main.o(.data) *(.data) } >RAM AT>FLASH .bss : { . = ALIGN(4); PROVIDE( _sbss = .); *(.sbss*) *(.gnu.linkonce.sb.*) *(.bss*) *(.gnu.linkonce.b.*) *(COMMON*) . = ALIGN(4); PROVIDE( _ebss = .); } >RAM AT>FLASH PROVIDE( _end = _ebss); PROVIDE( end = . ); /* 定义heap起始位置 */ .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : { PROVIDE( _heap_end = . ); /* 定义heap结束位置,默认到栈底结束 */ . = ALIGN(4); PROVIDE(_susrstack = . ); . = . + __stack_size; PROVIDE( _eusrstack = .); } >RAM }

secname后至少要有1个空格。其中,名字前面的.可有可无,一般都会加上。

*(.data) 含义先前已说明, 特别注意的是,main.o(.data)先前已链接,此时就不会再链接,这样做的目的是可以将某些特殊的输入文件链接到地址前面。

>RAM AT>FLASH:.data段的内容存储至Flash中(AT>指定),但运行时会加载至RAM中(通常为初始化全局变量),即**.data段的VMA为RAM,LMA为Flash**。

.stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size ::指定了栈底地址_susrstack,即为RAM的末尾地址 - 分配的栈大小,而_eusrstack 指定的栈顶地址。

由于使用的是满减栈,在启动文件中可以看到将栈顶地址_eusrstack加载到了sp指针中:

la sp, _eusrstack

end为堆的起始地址(紧跟bss段之后),_heap_end为堆的结束地址,等于栈低地址_susrstack,各段存储示意图如下:

image-20220823114052481

即除去程序用到的data、bss段,剩下RAM空间即为动态数据段,供堆的动态使用。

当然,也可以显示指定堆的大小,如:

PROVIDE( _end = _ebss); PROVIDE( end = . ); /* 定义heap起始位置 */ PROVIDE( _heap_end = . + 0x400); /* 定义heap结束位置,长度为1KB */ .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : { . = ALIGN(4); PROVIDE(_susrstack = . ); /*ASSERT ((. > 0x20005000),"ERROR:No room left for the stack");*/ . = . + __stack_size; PROVIDE( _eusrstack = .); }

此外,链接脚本中定义了_end为堆的起始地址,_heap_end为堆的结束地址,因此我们需要在_sbrk函数中进行指定,malloc函数会调用_sbrk函数获取当前堆的末端地址(入口参数incr为需要申请内存堆的大小),若不指定则会始终返回-1。

注意:_sbrk(0)获取的才是当前堆的末端地址,而其他值表示获取的是调用之前堆的末端地址(此时新的堆末端地址为sbrk(incr) + incr)

void *_sbrk(ptrdiff_t incr) { extern char _end[]; extern char _heap_end[]; static char *curbrk = _end; if ((curbrk + incr _heap_end)) return NULL - 1; curbrk += incr; return curbrk - incr; } 2.8 KEEP

当链接器使用--gc-sections进行垃圾回收时,链接器可能将某些它认为没用的 section 过滤掉,此时就有必要强制让链接器保留一些特定的 section,KEEP()可以使得被标记section的内容不被清除(即防止被优化)。示例:

.fini : { KEEP(*(SORT_NONE(.fini))) . = ALIGN(4); } >FLASH AT>FLASH 2.9 ALIGN

表示字节对齐, 如 . = ALIGN(4)表示从该地址开始后面的存储进行4字节对齐。

2.10 SORT_NONE

忽略 ld 命令行对满足字符串模式的所有名字进行递增排序的要求。e.g.三个源文件 DemoA.c,DemoB.c 和 DemoC.c,分别对其.text段使用SORT_NONE与SORT命令,即:

INPUT(DemoB.o) INPUT(DemoA.o) INPUT(DemoC.o) SORT_NONE(*)(.text) SORT(*)(.text)

image-20220823102628944

可以看到,使用SORT_NONE后按照我们导入目标文件的顺序进行链接,而使用SORT后则按照字符递增顺序链接。

2.11 ASSERT

语法:ASSERT(exp, message),确保exp是非零值,如果为零,将以错误码的形式退出链接文件,并输出message。主要用于添加断言,定位问题。

/* The usage of ASSERT */ PROVIDE (__stack_size = 0x100); .stack { PROVIDE (__stack = .); ASSERT ((__stack > (_end + __stack_size)), "Error: No room left for the stack"); } /* 当"__stack" 大于 "_end + __stack_size"时,在链接时,会出现错误,并提示"Error: No room left for the stack" */ 2.12 EXCLUDE_FILE

语法:EXCLUDE_FILE(FILENAME1 FILENAME2)剔除指定的输入文件,示例:

KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors))

即去除crtend.o与crtend?.o 目标文件的.dtors段。

3 使用示例

以CH32V103为例:

ENTRY( _start ) __stack_size = 2048; PROVIDE( _stack_size = __stack_size ); MEMORY { FLASH (rx) : ORIGIN = 0x00000000, LENGTH = 64K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K } SECTIONS { .init : { _sinit = .; . = ALIGN(4); KEEP(*(SORT_NONE(.init))) . = ALIGN(4); _einit = .; } >FLASH AT>FLASH .vector : { *(.vector); . = ALIGN(64); } >FLASH AT>FLASH .text : { . = ALIGN(4); *(.text) *(.text.*) *(.rodata) *(.rodata*) *(.glue_7) *(.glue_7t) *(.gnu.linkonce.t.*) . = ALIGN(4); PROVIDE(__ctors_start__ = .); /* C++构造函数初始化列表起始地址 */ KEEP (*(SORT(.init_array.*))) KEEP (*(.init_array)) PROVIDE(__ctors_end__ = .); /* C++构造函数初始化列表结束地址 */ . = ALIGN(4); } >FLASH AT>FLASH .fini : { KEEP(*(SORT_NONE(.fini))) . = ALIGN(4); } >FLASH AT>FLASH PROVIDE( _etext = . ); PROVIDE( _eitcm = . ); .preinit_array : { PROVIDE_HIDDEN (__preinit_array_start = .); KEEP (*(.preinit_array)) PROVIDE_HIDDEN (__preinit_array_end = .); } >FLASH AT>FLASH .init_array : { PROVIDE_HIDDEN (__init_array_start = .); KEEP (*(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*))) KEEP (*(.init_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .ctors)) PROVIDE_HIDDEN (__init_array_end = .); } >FLASH AT>FLASH .fini_array : { PROVIDE_HIDDEN (__fini_array_start = .); KEEP (*(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*))) KEEP (*(.fini_array EXCLUDE_FILE (*crtbegin.o *crtbegin?.o *crtend.o *crtend?.o ) .dtors)) PROVIDE_HIDDEN (__fini_array_end = .); } >FLASH AT>FLASH .ctors : { KEEP (*crtbegin.o(.ctors)) KEEP (*crtbegin?.o(.ctors)) KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .ctors)) KEEP (*(SORT(.ctors.*))) KEEP (*(.ctors)) } >FLASH AT>FLASH .dtors : { KEEP (*crtbegin.o(.dtors)) KEEP (*crtbegin?.o(.dtors)) KEEP (*(EXCLUDE_FILE (*crtend.o *crtend?.o ) .dtors)) KEEP (*(SORT(.dtors.*))) KEEP (*(.dtors)) } >FLASH AT>FLASH .dalign : { . = ALIGN(4); PROVIDE(_data_vma = .); /* data段运行内存起始地址 */ } >RAM AT>FLASH .dlalign : { . = ALIGN(4); PROVIDE(_data_lma = .); /* data段加载内存起始地址 */ } >FLASH AT>FLASH .data : { *(.gnu.linkonce.r.*) *(.data .data.*) /* 等价于*(.data.*) */ *(.gnu.linkonce.d.*) . = ALIGN(8); PROVIDE( __global_pointer$ = . + 0x800 ); /* 定义全局指针gp地址「0x800 = 2K」*/ *(.sdata .sdata.*) *(.sdata2.*) *(.gnu.linkonce.s.*) . = ALIGN(8); *(.srodata.cst16) *(.srodata.cst8) *(.srodata.cst4) *(.srodata.cst2) *(.srodata .srodata.*) . = ALIGN(4); PROVIDE( _edata = .); /* data段结束地址 */ } >RAM AT>FLASH .bss : { . = ALIGN(4); PROVIDE( _sbss = .); /* bss段起始地址 */ *(.sbss*) *(.gnu.linkonce.sb.*) *(.bss*) *(.gnu.linkonce.b.*) *(COMMON*) . = ALIGN(4); PROVIDE( _ebss = .); /* bss段结束地址 */ } >RAM AT>FLASH PROVIDE( _end = _ebss); PROVIDE( end = . ); /* 定义heap起始位置 */ .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : { PROVIDE( _heap_end = . ); /* 定义heap结束位置,默认到栈底结束 */ . = ALIGN(4); PROVIDE(_susrstack = . ); /* 定义stack栈低地址*/ . = . + __stack_size; PROVIDE( _eusrstack = .); /* 定义stack栈顶地址*/ } >RAM }

这里主要说明下data段及其重定位搬运操作,重点关注以下几个符号:

_data_vma定义了data段运行内存起始地址(RAM)_data_lma定义了data段加载内存起始地址(Flash)_edata则为data段结束地址__global_pointer$定义了全局指针寄存器gp的地址,通过gp指针,访问其值±2KB,即4KB范围内的全局变量,可以节约一条指令。

linker时使用__global_pointer$来比较全局变量的地址,如果在范围内,就替换掉lui或puipc指令的绝对寻址或pc相对寻址,变为gp相对寻址,使得代码效率更高。该过程被称为linker relaxation(链接器松弛),也可以使用-Wl,--no-relax来关闭此功能。

4KB区域可以位于寻址内存中任意位置,但是为了使优化更有效率,最好覆盖最频繁使用的RAM区域。 .sdata段与.sdata2段使用“小数据”寻址,即使用较短的地址访问。因此,如果将经常使用的数据放入其中,代码大小与执行时间将会减少。所以,__global_pointer$定义放在了 .sdata段前。

注意:gp寄存器在启动代码中加载为__global_pointer$的地址,并且之后不能被改变。此外,有时候为了优化代码密度,可以根据实际情况修改gp指针的位置,如工程中定义了大量的初始化为0或未初始化的全局数组作为缓冲区,可以将gp指针的位置定义到bss段。

ch32v103启动文件中gp指针地址加载与data段搬运操作汇编代码如下:

handle_reset: .option push .option norelax la gp, __global_pointer$ .option pop la sp, _eusrstack 2: /* Load data section from flash to RAM */ la a0, _data_lma ; data段加载内存起始地址 加载至a0 la a1, _data_vma ; data段运行内存起始地址 加载至a1 la a2, _edata ; data段结束地址 加载至a2 bgeu a1, a2, 2f ; 若a1 >= a2,则跳转到2处 1: lw t0, (a0) ; 将a0中的数据 加载到 t0 sw t0, (a1) ; 将t0中的数据 加载到 a1 addi a0, a0, 4 ; a0 += 4 addi a1, a1, 4 ; a1 += 4 bltu a1, a2, 1b ; 若a1 < a2,则跳转到1处 2: [...]

.option norelax表示不支持链接器松弛,但仅在push与pop中间这一行,并不是全局的。因为.option push的作用是将当前设置入栈,随后.option pop 又将入栈的设置弹了出来。松弛链接需要gp寄存器,在代码刚启动时gp寄存器还没有设置,因此在配置la gp, __global_pointer$,需要暂时禁用。

若想全局禁用,可采用如下设置(但会导致代码空间变大,不推荐使用):

img

References:

RISC-V MCU堆栈机制浅谈RISC-V GCC之:链接脚本学习笔记(二)链接脚本文件(.ld .lds)详解 Executable and Linkable Formatmalloc实现中与sbrk函数的关系RISC-V MCU ld链接脚本说明 – 以CH32V103为例RISC-V gp全局指针寄存器说明The gp (Global Pointer) register

END



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭